﻿using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Audio;
using System.IO;

namespace Platformer
{
	/// <summary>
	/// A uniform grid of tiles with collections of gems and enemies.
	/// The level owns the player and controls the game's win and lose
	/// conditions as well as scoring.
	/// </summary>
	class Level : IDisposable
	{
		// Physical structure of the level.
		private Tile[,] tiles;
		private Texture2D[] layers;
		// The layer which entities are drawn on top of.
		private const int EntityLayer = 2;

		// Entities in the level.
		public Player Player
		{
			get { return player; }
		}
		Player player;

		private List<Gem> gems = new List<Gem>();
		private List<Enemy> enemies = new List<Enemy>();

		// Key locations in the level.        
		private Vector2 start;
		private Point exit = InvalidPosition;
		private static readonly Point InvalidPosition = new Point(-1, -1);

		// Level game state.
		private Random random = new Random(354668); // Arbitrary, but constant seed

		public int Score
		{
			get { return score; }
		}
		int score;

		public bool ReachedExit
		{
			get { return reachedExit; }
		}
		bool reachedExit;

		public TimeSpan TimeRemaining
		{
			get { return timeRemaining; }
		}
		TimeSpan timeRemaining;

		private const int PointsPerSecond = 5;

		// Level content.        
		public ContentManager Content
		{
			get { return content; }
		}
		ContentManager content;

		private SoundEffect exitReachedSound;

		#region Loading

		/// <summary>
		/// Constructs a new level.
		/// </summary>
		/// <param name="serviceProvider">
		/// The service provider that will be used to construct a ContentManager.
		/// </param>
		/// <param name="path">
		/// The absolute path to the level file to be loaded.
		/// </param>
		public Level(IServiceProvider serviceProvider, string path)
		{
			// Create a new content manager to load content used just by this level.
			content = new ContentManager(serviceProvider, "Content");

			timeRemaining = TimeSpan.FromMinutes(2.0);

			LoadTiles(path);

			// Load background layer textures. For now, all levels must
			// use the same backgrounds and only use the left-most part of them.
			layers = new Texture2D[3];
			for (int i = 0; i < layers.Length; ++i)
			{
				// Choose a random segment if each background layer for level variety.
				int segmentIndex = random.Next(3);
				layers[i] = Content.Load<Texture2D>("Backgrounds/Layer" + i + "_" + segmentIndex);
			}

			// Load sounds.
			exitReachedSound = Content.Load<SoundEffect>("Sounds/ExitReached");
		}

		/// <summary>
		/// Iterates over every tile in the structure file and loads its
		/// appearance and behavior. This method also validates that the
		/// file is well-formed with a player start point, exit, etc.
		/// </summary>
		/// <param name="path">
		/// The absolute path to the level file to be loaded.
		/// </param>
		private void LoadTiles(string path)
		{
			// Load the level and ensure all of the lines are the same length.
			int width;
			List<string> lines = new List<string>();
			using (StreamReader reader = new StreamReader(App.GetResourceStream(new Uri(path, UriKind.Relative)).Stream))
			{
				string line = reader.ReadLine();
				width = line.Length;
				while (line != null)
				{
					lines.Add(line);
					if (line.Length != width)
						throw new Exception(String.Format("The length of line {0} is different from all preceeding lines.", lines.Count));
					line = reader.ReadLine();
				}
			}

			// Allocate the tile grid.
			tiles = new Tile[width, lines.Count];

			// Loop over every tile position,
			for (int y = 0; y < Height; ++y)
			{
				for (int x = 0; x < Width; ++x)
				{
					// to load each tile.
					char tileType = lines[y][x];
					tiles[x, y] = LoadTile(tileType, x, y);
				}
			}

			// Verify that the level has a beginning and an end.
			if (Player == null)
				throw new NotSupportedException("A level must have a starting point.");
			if (exit == InvalidPosition)
				throw new NotSupportedException("A level must have an exit.");

		}

		/// <summary>
		/// Loads an individual tile's appearance and behavior.
		/// </summary>
		/// <param name="tileType">
		/// The character loaded from the structure file which
		/// indicates what should be loaded.
		/// </param>
		/// <param name="x">
		/// The X location of this tile in tile space.
		/// </param>
		/// <param name="y">
		/// The Y location of this tile in tile space.
		/// </param>
		/// <returns>The loaded tile.</returns>
		private Tile LoadTile(char tileType, int x, int y)
		{
			switch (tileType)
			{
				// Blank space
				case '.':
					return new Tile(null, TileCollision.Passable);

				// Exit
				case 'X':
					return LoadExitTile(x, y);

				// Gem
				case 'G':
					return LoadGemTile(x, y);

				// Floating platform
				case '-':
					return LoadTile("Platform", TileCollision.Platform);

				// Various enemies
				case 'A':
					return LoadEnemyTile(x, y, "MonsterA");
				case 'B':
					return LoadEnemyTile(x, y, "MonsterB");
				case 'C':
					return LoadEnemyTile(x, y, "MonsterC");
				case 'D':
					return LoadEnemyTile(x, y, "MonsterD");

				// Platform block
				case '~':
					return LoadVarietyTile("BlockB", 2, TileCollision.Platform);

				// Passable block
				case ':':
					return LoadVarietyTile("BlockB", 2, TileCollision.Passable);

				// Player 1 start point
				case '1':
					return LoadStartTile(x, y);

				// Impassable block
				case '#':
					return LoadVarietyTile("BlockA", 7, TileCollision.Impassable);

				// Unknown tile type character
				default:
					throw new NotSupportedException(String.Format("Unsupported tile type character '{0}' at position {1}, {2}.", tileType, x, y));
			}
		}

		/// <summary>
		/// Creates a new tile. The other tile loading methods typically chain to this
		/// method after performing their special logic.
		/// </summary>
		/// <param name="name">
		/// Path to a tile texture relative to the Content/Tiles directory.
		/// </param>
		/// <param name="collision">
		/// The tile collision type for the new tile.
		/// </param>
		/// <returns>The new tile.</returns>
		private Tile LoadTile(string name, TileCollision collision)
		{
			return new Tile(Content.Load<Texture2D>("Tiles/" + name), collision);
		}


		/// <summary>
		/// Loads a tile with a random appearance.
		/// </summary>
		/// <param name="baseName">
		/// The content name prefix for this group of tile variations. Tile groups are
		/// name LikeThis0.png and LikeThis1.png and LikeThis2.png.
		/// </param>
		/// <param name="variationCount">
		/// The number of variations in this group.
		/// </param>
		private Tile LoadVarietyTile(string baseName, int variationCount, TileCollision collision)
		{
			int index = random.Next(variationCount);
			return LoadTile(baseName + index, collision);
		}


		/// <summary>
		/// Instantiates a player, puts him in the level, and remembers where to put him when he is resurrected.
		/// </summary>
		private Tile LoadStartTile(int x, int y)
		{
			if (Player != null)
				throw new NotSupportedException("A level may only have one starting point.");

			start = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
			player = new Player(this, start);

			return new Tile(null, TileCollision.Passable);
		}

		/// <summary>
		/// Remembers the location of the level's exit.
		/// </summary>
		private Tile LoadExitTile(int x, int y)
		{
			if (exit != InvalidPosition)
				throw new NotSupportedException("A level may only have one exit.");

			exit = GetBounds(x, y).Center;

			return LoadTile("Exit", TileCollision.Passable);
		}

		/// <summary>
		/// Instantiates an enemy and puts him in the level.
		/// </summary>
		private Tile LoadEnemyTile(int x, int y, string spriteSet)
		{
			Vector2 position = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
			enemies.Add(new Enemy(this, position, spriteSet));

			return new Tile(null, TileCollision.Passable);
		}

		/// <summary>
		/// Instantiates a gem and puts it in the level.
		/// </summary>
		private Tile LoadGemTile(int x, int y)
		{
			Point position = GetBounds(x, y).Center;
			gems.Add(new Gem(this, new Vector2(position.X, position.Y)));

			return new Tile(null, TileCollision.Passable);
		}

		/// <summary>
		/// Unloads the level content.
		/// </summary>
		public void Dispose()
		{
			Content.Unload();
		}

		#endregion

		#region Bounds and collision

		/// <summary>
		/// Gets the collision mode of the tile at a particular location.
		/// This method handles tiles outside of the levels boundries by making it
		/// impossible to escape past the left or right edges, but allowing things
		/// to jump beyond the top of the level and fall off the bottom.
		/// </summary>
		public TileCollision GetCollision(int x, int y)
		{
			// Prevent escaping past the level ends.
			if (x < 0 || x >= Width)
				return TileCollision.Impassable;
			// Allow jumping past the level top and falling through the bottom.
			if (y < 0 || y >= Height)
				return TileCollision.Passable;

			return tiles[x, y].Collision;
		}

		/// <summary>
		/// Gets the bounding rectangle of a tile in world space.
		/// </summary>        
		public Rectangle GetBounds(int x, int y)
		{
			return new Rectangle(x * Tile.Width, y * Tile.Height, Tile.Width, Tile.Height);
		}

		/// <summary>
		/// Width of level measured in tiles.
		/// </summary>
		public int Width
		{
			get { return tiles.GetLength(0); }
		}

		/// <summary>
		/// Height of the level measured in tiles.
		/// </summary>
		public int Height
		{
			get { return tiles.GetLength(1); }
		}

		#endregion

		#region Update

		/// <summary>
		/// Updates all objects in the world, performs collision between them,
		/// and handles the time limit with scoring.
		/// </summary>
		public void Update(GameTime gameTime)
		{
			// Pause while the player is dead or time is expired.
			if (!Player.IsAlive || TimeRemaining == TimeSpan.Zero)
			{
				// Still want to perform physics on the player.
				Player.ApplyPhysics(gameTime);
			}
			else if (ReachedExit)
			{
				// Animate the time being converted into points.
				int seconds = (int)Math.Round(gameTime.ElapsedGameTime.TotalSeconds * 100.0f);
				seconds = Math.Min(seconds, (int)Math.Ceiling(TimeRemaining.TotalSeconds));
				timeRemaining -= TimeSpan.FromSeconds(seconds);
				score += seconds * PointsPerSecond;
			}
			else
			{
				timeRemaining -= gameTime.ElapsedGameTime;

				Player.Update(gameTime);

				UpdateGems(gameTime);

				// Falling off the bottom of the level kills the player.
				if (Player.BoundingRectangle.Top >= Height * Tile.Height)
					OnPlayerKilled(null);

				UpdateEnemies(gameTime);

				// The player has reached the exit if they are standing on the ground and
				// his bounding rectangle contains the center of the exit tile. They can only
				// exit when they have collected all of the gems.
				if (Player.IsAlive &&
					Player.IsOnGround &&
					Player.BoundingRectangle.Contains(exit))
				{
					OnExitReached();
				}
			}

			// Clamp the time remaining at zero.
			if (timeRemaining < TimeSpan.Zero)
				timeRemaining = TimeSpan.Zero;
		}

		/// <summary>
		/// Animates each gem and checks to allows the player to collect them.
		/// </summary>
		private void UpdateGems(GameTime gameTime)
		{
			for (int i = 0; i < gems.Count; ++i)
			{
				Gem gem = gems[i];

				gem.Update(gameTime);

				if (gem.BoundingCircle.Intersects(Player.BoundingRectangle))
				{
					gems.RemoveAt(i--);
					OnGemCollected(gem, Player);
				}
			}
		}

		/// <summary>
		/// Animates each enemy and allow them to kill the player.
		/// </summary>
		private void UpdateEnemies(GameTime gameTime)
		{
			foreach (Enemy enemy in enemies)
			{
				enemy.Update(gameTime);

				// Touching an enemy instantly kills the player
				if (enemy.BoundingRectangle.Intersects(Player.BoundingRectangle))
				{
					OnPlayerKilled(enemy);
				}
			}
		}

		/// <summary>
		/// Called when a gem is collected.
		/// </summary>
		/// <param name="gem">The gem that was collected.</param>
		/// <param name="collectedBy">The player who collected this gem.</param>
		private void OnGemCollected(Gem gem, Player collectedBy)
		{
			score += Gem.PointValue;

			gem.OnCollected(collectedBy);
		}

		/// <summary>
		/// Called when the player is killed.
		/// </summary>
		/// <param name="killedBy">
		/// The enemy who killed the player. This is null if the player was not killed by an
		/// enemy, such as when a player falls into a hole.
		/// </param>
		private void OnPlayerKilled(Enemy killedBy)
		{
			Player.OnKilled(killedBy);
		}

		/// <summary>
		/// Called when the player reaches the level's exit.
		/// </summary>
		private void OnExitReached()
		{
			Player.OnReachedExit();
			exitReachedSound.Play();
			reachedExit = true;
		}

		/// <summary>
		/// Restores the player to the starting point to try the level again.
		/// </summary>
		public void StartNewLife()
		{
			Player.Reset(start);
		}

		#endregion

		#region Draw

		/// <summary>
		/// Draw everything in the level from background to foreground.
		/// </summary>
		public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
		{
			for (int i = 0; i <= EntityLayer; ++i)
				spriteBatch.Draw(layers[i], Vector2.Zero, Color.White);

			DrawTiles(spriteBatch);

			foreach (Gem gem in gems)
				gem.Draw(gameTime, spriteBatch);

			Player.Draw(gameTime, spriteBatch);

			foreach (Enemy enemy in enemies)
				enemy.Draw(gameTime, spriteBatch);

			for (int i = EntityLayer + 1; i < layers.Length; ++i)
				spriteBatch.Draw(layers[i], Vector2.Zero, Color.White);
		}

		/// <summary>
		/// Draws each tile in the level.
		/// </summary>
		private void DrawTiles(SpriteBatch spriteBatch)
		{
			// For each tile position
			for (int y = 0; y < Height; ++y)
			{
				for (int x = 0; x < Width; ++x)
				{
					// If there is a visible tile in that position
					Texture2D texture = tiles[x, y].Texture;
					if (texture != null)
					{
						// Draw it in screen space.
						Vector2 position = new Vector2(x, y) * Tile.Size;
						spriteBatch.Draw(texture, position, Color.White);
					}
				}
			}
		}

		#endregion
	}
}
